﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Reflection;

namespace SampleImageGenerator.Serialisation
{
	/// <summary>
	/// <para>
	/// This class was written to provide similar functionality to the .NET 
	/// XmlSerializer class, but to overcome some of the limitations I found
	/// when I wanted to be able to serialise IModule objects to a "human
	/// readable" format. It is not as fully featured as the .NET class, and is
	/// only intended to be able to serialise simple objects. 
	/// </para>
	/// <para>
	/// Like the .NET XML Serializer, this class exports or imports all the 
	/// public properties of a type, and relies on a parameterless constructor
	/// to initialise new instances of a type. 
	/// </para>
	/// </summary>
	/// <remarks>
	/// <para>
	/// There are some rules that objects to be serialised must conform to. 
	/// <list>
	/// <item>
	/// The object to be serialised must have a parameterless constructor. This 
	/// is required when deserialising as the object is created using this 
	/// constructor before having its property values assigned. 
	/// </item>
	/// <item>
	/// The root object to be serialised must be an object, not a primitive 
	/// type. 
	/// </item>
	/// <item>
	/// The only kind of collection that can be serialised is an array.
	/// </item>
	/// <item>
	/// Indexer properties are not supported. 
	/// </item>
	/// </list>
	/// </para>
	/// <para>
	/// The key problems I found with the .NET XmlSerializer class are listed 
	/// below. This class aims to address these concerns. 
	/// <list>
	/// <item>
	/// The serialiser could only serialise types that were specially marked. 
	/// In particular, if a property was a base type or an interface, special
	/// attributes needed to be applied listing all the kinds of types that 
	/// could be assigned to that property before objects of those types could
	/// be serialised. I thought that this would be too much of an overhead 
	/// when trying to serialise types of IModule, for example. 
	/// </item>
	/// <item>
	/// The serialiser needed to be constructed knowing the type of the root 
	/// object that was to be serialised or deserialised. Again, this had to be 
	/// the actual object type, not a base class. This would have required me 
	/// to store the type of each serialised object ahead of its serialised 
	/// data, which would have meant producing a document that was not pure XML. 
	/// </item>
	/// </list>
	/// </para>
	/// </remarks>
	internal class XmlSerialiser
	{
		private const String OBJECT_ELEMENT = "object";
		private const String TYPE_ATTRIBUTE = "type";

		private const String BYTE = "System.Byte";
		private const String SBYTE = "System.SByte";
		private const String INT32 = "System.Int32";
		private const String UINT32 = "System.UInt32";
		private const String INT16 = "System.Int16";
		private const String UINT16 = "System.UInt16";
		private const String INT64 = "System.Int64";
		private const String UINT64 = "System.UInt64";
		private const String SINGLE = "System.Single";
		private const String DOUBLE = "System.Double";
		private const String CHAR = "System.Char";
		private const String BOOLEAN = "System.Boolean";
		private const String STRING = "System.String";
		private const String DECIMAL = "System.Decimal";
		private const String OBJECT = "System.Object"; 

		public void Serialise(TextWriter text, Object obj)
		{
			XmlWriterSettings settings = new XmlWriterSettings();
			settings.Indent = true;
			XmlWriter writer = XmlWriter.Create(text, settings);
			writer.WriteStartDocument();

			SerialiseItem(writer, obj, OBJECT_ELEMENT, true);

			writer.WriteEndDocument();
			writer.Flush();
		}

		public Object Deserialise(TextReader text)
		{
		    XmlReader reader = XmlReader.Create(text);
			XmlDocument document = new XmlDocument();
			document.Load(reader);
			XmlNode objectNode = document.SelectSingleNode(OBJECT_ELEMENT);
			Object result = DeserialiseItem(objectNode, typeof(Object));

			return result; 
		}

		private Object DeserialiseItem(XmlNode itemNode, Type implicitType)
		{
			Type explicitType;
			Object result;

			XmlAttribute typeAttribute = itemNode.Attributes[TYPE_ATTRIBUTE];
			if (typeAttribute != null)
			{
				String typeName = typeAttribute.Value;
				explicitType = Type.GetType(typeName);
			}
			else
			{
				explicitType = implicitType;
			}

			if (explicitType.IsPrimitive || explicitType.FullName == STRING)
			{
				result = DeserialisePrimitive(itemNode, explicitType);
			}
			else if (explicitType.IsArray)
			{
				result = DeserialiseArray(itemNode, explicitType);
			}
			else if (explicitType.IsEnum)
			{
				result = DeserialiseEnum(itemNode, explicitType);
			}
			else if (explicitType.IsClass || explicitType.IsValueType)
			{
				// Deserialise classes and structs in the same way. 
				result = DeserialiseObject(itemNode, explicitType);
			}
			else
			{
				String message = String.Format("Unable to deserialise items of type {0}", implicitType);
				throw new InvalidOperationException(message);
			}
			
			return result; 
		}

		private object DeserialiseObject(XmlNode objectNode, Type objectType)
		{
			Object obj = Activator.CreateInstance(objectType);

			foreach (XmlNode propertyNode in objectNode.ChildNodes)
			{

				if (propertyNode.NodeType == XmlNodeType.Element)
				{
					String propertyName = propertyNode.Name;
					PropertyInfo propertyInfo = objectType.GetProperty(propertyName);
					if (propertyInfo.CanWrite)
					{
						Type propertyType = propertyInfo.PropertyType;
						Object propertyValue = DeserialiseItem(propertyNode, propertyType);
						propertyInfo.SetValue(obj, propertyValue, null);
					}
				}
			}

			return obj; 
		}

		private object DeserialiseArray(XmlNode objectNode, Type arrayType)
		{
			Type arrayElementType = arrayType.GetElementType();
			List<Object> elements = new List<Object>();
			foreach (XmlNode elementNode in objectNode.ChildNodes)
			{
				if (elementNode.NodeType == XmlNodeType.Element)
				{
					// If the element is a complex type, the type
					// will be recorded in the object node. If it's 
					// a primitve, or an enum, it's not recoded, but 
					// we already know the array element type, so we 
					// can use that. 
					Type elementType;
					XmlAttribute typeAttribute = objectNode.Attributes[TYPE_ATTRIBUTE];
					if (typeAttribute != null)
					{
						String typeName = typeAttribute.Value;
						elementType = Type.GetType(typeName);
					}
					else
					{
						elementType = arrayElementType;
					}

					Object elementValue = DeserialiseItem(elementNode, elementType);
					elements.Add(elementValue);
				}
			}

			// Now that we've extracted all the objects from the XML, we 
			// can put them into an array of the correct type. 
			Int32 elementCount = elements.Count;
			Array array = Array.CreateInstance(arrayElementType, elementCount);
			for (Int32 index = 0; index < elementCount; index++)
			{
				Object elementValue = elements[index];
				array.SetValue(elementValue, index);
			}

			return array;
		}
		
		private object DeserialiseEnum(XmlNode objectNode, Type enumType)
		{
			String xmlValue = objectNode.InnerText;
			Object enumValue = Enum.Parse(enumType, xmlValue);
			return enumValue;
		}

		private object DeserialisePrimitive(XmlNode propertyNode, Type propertyType)
		{
			Object propertyValue;
			switch (propertyType.FullName)
			{
				case BYTE:
					propertyValue = Convert.ToByte(propertyNode.InnerText);
					break;

				case SBYTE:
					propertyValue = Convert.ToSByte(propertyNode.InnerText);
					break;

				case INT32:
					propertyValue = Convert.ToInt32(propertyNode.InnerText);
					break;

				case UINT32:
					propertyValue = Convert.ToUInt32(propertyNode.InnerText);
					break;

				case INT16:
					propertyValue = Convert.ToInt16(propertyNode.InnerText);
					break;

				case UINT16:
					propertyValue = Convert.ToUInt16(propertyNode.InnerText);
					break;

				case INT64:
					propertyValue = Convert.ToInt64(propertyNode.InnerText);
					break;

				case UINT64:
					propertyValue = Convert.ToUInt64(propertyNode.InnerText);
					break;

				case SINGLE:
					propertyValue = Convert.ToSingle(propertyNode.InnerText);
					break;

				case DOUBLE:
					propertyValue = Convert.ToDouble(propertyNode.InnerText);
					break;

				case CHAR:
					propertyValue = Convert.ToChar(propertyNode.InnerText);
					break;

				case BOOLEAN:
					propertyValue = Convert.ToBoolean(propertyNode.InnerText);
					break;

				case STRING:
					propertyValue = Convert.ToString(propertyNode.InnerText);
					break;

				case DECIMAL:
					propertyValue = Convert.ToDecimal(propertyNode.InnerText);
					break;

				default:
					String message = String.Format("Unexpected primitive type {0}", propertyType);
					throw new InvalidOperationException(message);
			}

			return propertyValue;
		}
	
		private void SerialiseItem(XmlWriter writer, Object obj, String name, Boolean requireTypeName)
		{
			// When we serialise an object, we need to record its type so that 
			// we can reconstruct it again later. 
			//
			// For primitive types, we don't need to do this, as we can 
			// reflect on the object that we have created to determine the types
			// of each property. This helps keep the XML cleaner. 
			Type type = obj.GetType();

			if (type.IsPrimitive || type.FullName == STRING)
			{
				SerialisePrimitive(writer, obj, name, requireTypeName);
			}
			else if (type.IsEnum)
			{
				SerialiseEnum(writer, obj, name, requireTypeName);
			}
			else if (type.IsArray)
			{
				SerialiseArray(writer, obj, name);
			}
			else
			{
				SerialiseObject(writer, obj, name);
			}
		}

		private void SerialiseObject(XmlWriter writer, Object obj, String name)
		{
			// It's not a primitive type, so record what kind it is,
			// then serialise all its properties.
			writer.WriteStartElement(name);
			WriteTypeAttribute(writer, obj);
			SerialiseProperties(writer, obj);
			writer.WriteEndElement();
		}

		private void SerialisePrimitive(XmlWriter writer, Object obj, String name, Boolean requireTypeName)
		{
			writer.WriteStartElement(name);
			if (requireTypeName)
			{
				WriteTypeAttribute(writer, obj);
			}
			writer.WriteValue(obj);
			writer.WriteEndElement();
		}

		private void SerialiseEnum(XmlWriter writer, Object obj, String name, Boolean requireTypeName)
		{
			writer.WriteStartElement(name);
			if (requireTypeName)
			{
				WriteTypeAttribute(writer, obj);
			}
			String enumValue = obj.ToString();
			writer.WriteValue(enumValue);
			writer.WriteEndElement();
		}

		private void SerialiseArray(XmlWriter writer, Object obj, String name)
		{
			Type type = obj.GetType();
			Int32 rank = type.GetArrayRank();
			if (rank != 1)
			{
				throw new InvalidOperationException("Only simple arrays are supported"); 
			}

			writer.WriteStartElement(name);

			Boolean requireTypeName = false;
			Type elementType = type.GetElementType();
			if (elementType.FullName == OBJECT)
			{
				requireTypeName = true; 
			}

			Object[] values = (Object[])obj;
			foreach (Object value in values)
			{
				SerialiseItem(writer, value, "Item", requireTypeName); 
			}

			writer.WriteEndElement();
		}

		private void SerialiseProperties(XmlWriter writer, Object obj)
		{
			Type type = obj.GetType();
			PropertyInfo[] propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
			Array.Sort<PropertyInfo>(propertyInfos, PropertyComparer);

			foreach (PropertyInfo propertyInfo in propertyInfos)
			{
				String name = propertyInfo.Name;
				if (propertyInfo.CanWrite)
				{
					Object value = propertyInfo.GetValue(obj, null);
					SerialiseItem(writer, value, name, false);
				}
			}
		}

		private void WriteTypeAttribute(XmlWriter writer, Object obj)
		{
			Type type = obj.GetType();
			Assembly declaringAssembly = type.Assembly;
			Assembly mscorlibAssembly = Assembly.GetAssembly(typeof(Object));
			Assembly thisAssembly = Assembly.GetExecutingAssembly();

			String typeName;
			if (declaringAssembly.Equals(mscorlibAssembly) || declaringAssembly.Equals(thisAssembly))
			{
				typeName = type.FullName;
			}
			else
			{
				typeName = type.AssemblyQualifiedName;
			}

			writer.WriteAttributeString(TYPE_ATTRIBUTE, typeName);
		}

		private Int32 PropertyComparer(PropertyInfo x, PropertyInfo y)
		{
			Int32 result = String.Compare(x.Name, y.Name);
			return result; 
		}
	}
}
